package util.level;

import java.awt.Point;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import javax.xml.bind.DatatypeConverter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import util.instance.Instance;
import util.resource.ResourceManager;

public class LevelLoader
{
	private String roomName;
	private int width,height;
	private Color roomColor;
	private TileGroup backgroundTiles;
	private TileGroup foregroundTiles;
	private ArrayList<Instance> instanceList;
	
	public LevelLoader(String fileLocation) throws Exception
	{
		//All you have to do is take a cup of flour, add it to the mix.
		instanceList=new ArrayList<Instance>();
		backgroundTiles=new TileGroup();
		foregroundTiles=new TileGroup();
		
		//Get ourselves an XML Reader, baby.
		XMLInputFactory factory=XMLInputFactory.newFactory();
		XMLStreamReader reader=factory.createXMLStreamReader(new FileInputStream(fileLocation));
		
		//Great, keep it up. Now let's make some variables.
		//By the way, who the heck rhymed "vanilla" with "filla"?
		String tag="";
		boolean fg=true;
		TileLayer lastLayer=null;
		Instance lastInstance=null;
		
		//Now draw the rest of the owl.
		while(reader.hasNext())
		{
			int event=reader.next();
			switch(event)
			{
				case XMLStreamConstants.START_ELEMENT:
					tag=reader.getLocalName();
					
					if(tag.equals("level"))
					{
						for(int i=0;i<reader.getAttributeCount();i++)
						{
							if(reader.getAttributeLocalName(i).equals("width"))
								width=Integer.parseInt(reader.getAttributeValue(i));
							if(reader.getAttributeLocalName(i).equals("height"))
								height=Integer.parseInt(reader.getAttributeValue(i));
						}
					}
					
					if(tag.equals("layer"))
					{
						//defaults
						Image image=null;
						String name="";
						int tileWidth=16;
						int tileHeight=16;
						int tileSpacing=0;
						int layerWidth=16;
						int layerHeight=16;
						
						//set values from XML
						for(int i=0;i<reader.getAttributeCount();i++)
						{
							if(reader.getAttributeLocalName(i).equals("image"))
								image=ResourceManager.getImage(reader.getAttributeValue(i));
							else if(reader.getAttributeLocalName(i).equals("name"))
								name=reader.getAttributeValue(i);
							else if(reader.getAttributeLocalName(i).equals("tile_width"))
								tileWidth=Integer.parseInt(reader.getAttributeValue(i));
							else if(reader.getAttributeLocalName(i).equals("tile_height"))
								tileHeight=Integer.parseInt(reader.getAttributeValue(i));
							else if(reader.getAttributeLocalName(i).equals("tile_spacing"))
								tileSpacing=Integer.parseInt(reader.getAttributeValue(i));
							else if(reader.getAttributeLocalName(i).equals("layer_width"))
								layerWidth=Integer.parseInt(reader.getAttributeValue(i));
							else if(reader.getAttributeLocalName(i).equals("layer_height"))
								layerHeight=Integer.parseInt(reader.getAttributeValue(i));
						}
						
						//create tile layer
						if(image!=null)
							lastLayer=new TileLayer(image,name,tileWidth,tileHeight,tileSpacing,layerWidth,layerHeight);
						else
							throw new RuntimeException("Attribute 'image' not defined in tile layer!");
						
						if(fg)
							foregroundTiles.addLayer(lastLayer);
						else
							backgroundTiles.addLayer(lastLayer);
						
					}
					
					if(tag.equals("fg_tiles"))
						fg=true;
					if(tag.equals("bg_tiles"))
						fg=false;
					
					//Object loading
					if(tag.equals("object"))
					{
						String name="util.instance.Instance";
						Instance instance;
						for(int i=0;i<reader.getAttributeCount();i++)
							if(reader.getAttributeLocalName(i).equals("class"))
							{
								name=reader.getAttributeValue(i);
								break;
							}
						try
						{
							instance=getInstance(name);
							lastInstance=instance;
							instanceList.add(lastInstance);
						}
						catch(Exception e)
						{
							e.printStackTrace();
						}
					}
					
					if(tag.equals("var") && lastInstance!=null)
					{
						String key=""; //default values
						String value="0";
						for(int i=0;i<reader.getAttributeCount();i++)
						{
							if(reader.getAttributeLocalName(i).equals("key"))
								key=reader.getAttributeValue(i);
							if(reader.getAttributeLocalName(i).equals("value"))
								value=reader.getAttributeValue(i);
						}
						//assign key to value, todo,check types
						Field f=getField(lastInstance.getClass(),key);
						f.setAccessible(true);
						f.setFloat(lastInstance,Integer.parseInt(value));
							
					}
					
					break;
				case XMLStreamConstants.END_ELEMENT:
					tag=reader.getLocalName();
					if(tag.equals("layer"))
						lastLayer=null;
					if(tag.equals("object"))
						lastInstance=null;
					break;
				case XMLStreamConstants.CHARACTERS:
					if(lastLayer!=null && tag.equals("layer"))
						lastLayer.setTiles(decompressString(reader.getText(),lastLayer.getLayerWidthInTiles(),lastLayer.getLayerHeightInTiles()));
					break;
			}
		}
		
		reader.close();
		
	}
	
	public TileGroup getBackgroundTiles()
	{
		return backgroundTiles;
	}
	
	public TileGroup getForegroundTiles()
	{
		return foregroundTiles;
	}
	
	public ArrayList<Instance> getInstances()
	{
		return instanceList;
	}
	
	public String getName()
	{
		return roomName;
	}
	
	public Color getColor()
	{
		return roomColor;
	}
	
	public int getWidth()
	{
		return width;
	}
	
	public int getHeight()
	{
		return height;
	}
	
	
	//loading helper methods
	/**
	 * @param string - encoded string of the level.
	 * @param width - number of tiles in width of the room.
	 * @param height - number of tiles in height of the room.
	 * @return array of points representing the correct positions on the source sheet to display
	 * on the destination room.
	 */
	private Point[][] decompressString(String string,int width,int height)
	{	
		//Create the array to return.
		Point[][] tiles=new Point[width][height];
		
		try
		{
			//Convert the base64 string into bytes.
			byte[] byteArray=DatatypeConverter.parseBase64Binary(string);
			
			//Prepare for decompression.
			Inflater decompresser=new Inflater();
			decompresser.setInput(byteArray);
			ByteArrayOutputStream bos=new ByteArrayOutputStream(string.length());
			byte[] buffer=new byte[1024];
			
			//Full speed ahead!
			while(!decompresser.finished())
			{
				int count=decompresser.inflate(buffer);
				bos.write(buffer,0,count);
			}
			decompresser.end();
			
			//Byte array now contains a CSV string, so separate by commas.
			String levelString[]=new String(bos.toByteArray()).split(",");
			
			//Now parse the string of positions into an array of points.
			for(int y=0;y<height;y++)
				for(int x=0;x<width;x++)
				{
					int v1=Integer.parseInt(levelString[2*(y*width+x)]);
					int v2=Integer.parseInt(levelString[2*(y*width+x)+1]);
					if(v1!=-1&&v2!=-1)
						tiles[x][y]=new Point(v1,v2);
				}
		}
		catch(DataFormatException e)
		{
			//Well, the data should be valid so this should never happen.
			e.printStackTrace();
		}
		//Good job boys. Now go home.
		return tiles;
	}
	
	/**
	 * Private helper function which gets a field from a string and an object.
	 * @param object - object to get the field from.
	 * @param fieldName - name of the field to fetch.
	 * @return the field of the given object
	 * @throws NoSuchFieldException when the object has no field with the name fieldName
	 */
	private Field getField(Class<? extends Instance> object, String fieldName) throws NoSuchFieldException 
	{
		try 
			{return object.getDeclaredField(fieldName);} 
		catch (NoSuchFieldException e)
		{
			@SuppressWarnings("unchecked") //really no way around this
			Class<? extends Instance> superClass=(Class<? extends Instance>)object.getSuperclass();
		    if(superClass==null)
		    	throw e;
		    else
		        return getField(superClass, fieldName);
		}
	}
	
	/**
	 * Creates an object based on its name as a string.
	 * @param instanceName - the name of the class to create an object from.
	 * @return the generated instance
	 * @throws Exception honestly there are so many exceptions that this throws that catching them
	 * individually isn't worth it
	 */
	private Instance getInstance(String instanceName) throws Exception
	{
		@SuppressWarnings("unchecked") //really no way around this
		Class<Instance> objectClass=(Class<Instance>)Class.forName(instanceName);
		Constructor<Instance> constructor=objectClass.getConstructor(new Class[0]);
		
		Instance object=constructor.newInstance();
		
		return object;
	}
}
